/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.location;

import static java.util.concurrent.TimeUnit.NANOSECONDS;

import android.annotation.SuppressLint;
import android.location.Location;
import android.os.Build.VERSION;
import android.os.Bundle;

import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Helper for accessing features in {@link android.location.Location}.
 */
public final class LocationCompat {

    /**
     * Constant used as a key to store mock location status in {@link Location#getExtras()} for
     * Android SDK levels below JBMR2 (18).
     */
    @SuppressWarnings("ActionValue") // legacy value
    public static final String EXTRA_IS_MOCK = "mockLocation";

    /**
     * Constant used as a key to store vertical accuracy in {@link Location#getExtras()} for
     * Android SDK levels below Oreo (26).
     */
    @SuppressWarnings("ActionValue") // legacy value
    public static final String EXTRA_VERTICAL_ACCURACY = "verticalAccuracy";

    /**
     * Constant used as a key to store speed accuracy in {@link Location#getExtras()} for
     * Android SDK levels below Oreo (26).
     */
    @SuppressWarnings("ActionValue") // legacy value
    public static final String EXTRA_SPEED_ACCURACY = "speedAccuracy";

    /**
     * Constant used as a key to store bearing accuracy in {@link Location#getExtras()} for
     * Android SDK levels below Oreo (26).
     */
    @SuppressWarnings("ActionValue") // legacy value
    public static final String EXTRA_BEARING_ACCURACY = "bearingAccuracy";

    /**
     * Constant used as a key to store Mean Sea Level altitude in {@link Location#getExtras()}.
     */
    public static final String EXTRA_MSL_ALTITUDE = "androidx.core.location.extra.MSL_ALTITUDE";

    /**
     * Constant used as a key to store Mean Sea Level altitude in {@link Location#getExtras()}.
     */
    public static final String EXTRA_MSL_ALTITUDE_ACCURACY =
            "androidx.core.location.extra.MSL_ALTITUDE_ACCURACY";

    @Nullable
    private static Method sSetIsFromMockProviderMethod;
    @Nullable
    private static Field sFieldsMaskField;
    @Nullable
    private static Integer sHasSpeedAccuracyMask;
    @Nullable
    private static Integer sHasBearingAccuracyMask;
    @Nullable
    private static Integer sHasVerticalAccuracyMask;

    private LocationCompat() {
    }

    /**
     * Return the time of this fix, in nanoseconds of elapsed real-time since system boot.
     *
     * <p>This value can be reliably compared to SystemClock.elapsedRealtimeNanos(), to calculate
     * the age of a fix and to compare location fixes. This is reliable because elapsed real-time
     * is guaranteed monotonic for each system boot and continues to increment even when the
     * system is in deep sleep (unlike getTime().
     *
     * <p>All locations generated by the LocationManager are guaranteed to have a valid elapsed
     * real-time.
     *
     * <p>NOTE: On API levels below 17, this method will attempt to provide an elapsed realtime
     * based on the difference between system time and the location time. This should be taken as a
     * best "guess" at what the elapsed realtime might have been, but if the clock used for
     * location derivation is different from the system clock, the results may be inaccurate.
     * @deprecated Call {@link Location#getElapsedRealtimeNanos()} directly.
     */
    @Deprecated
    @androidx.annotation.ReplaceWith(expression = "location.getElapsedRealtimeNanos()")
    public static long getElapsedRealtimeNanos(@NonNull Location location) {
        return location.getElapsedRealtimeNanos();
    }

    /**
     * Return the time of this fix, in milliseconds of elapsed real-time since system boot.
     *
     * @see #getElapsedRealtimeNanos(Location)
     */
    public static long getElapsedRealtimeMillis(@NonNull Location location) {
        return NANOSECONDS.toMillis(location.getElapsedRealtimeNanos());
    }

    /**
     * Returns true if this location has a vertical accuracy.
     *
     * @see Location#hasVerticalAccuracy()
     */
    public static boolean hasVerticalAccuracy(@NonNull Location location) {
        if (VERSION.SDK_INT >= 26) {
            return Api26Impl.hasVerticalAccuracy(location);
        } else {
            return containsExtra(location, EXTRA_VERTICAL_ACCURACY);
        }
    }

    /**
     * Get the estimated vertical accuracy of this location in meters.
     *
     * <p>NOTE: On API levels below 26, the concept of vertical accuracy does not exist. In order to
     * allow for backwards compatibility and testing however, this method will attempt to read a
     * float extra with the key {@link #EXTRA_VERTICAL_ACCURACY} and return the result.
     *
     * @see Location#getVerticalAccuracyMeters()
     */
    public static float getVerticalAccuracyMeters(@NonNull Location location) {
        if (VERSION.SDK_INT >= 26) {
            return Api26Impl.getVerticalAccuracyMeters(location);
        } else {
            Bundle extras = location.getExtras();
            if (extras == null) {
                return 0.0f;
            }

            return extras.getFloat(EXTRA_VERTICAL_ACCURACY, 0.0f);
        }
    }

    /**
     * Set the estimated vertical accuracy of this location in meters.
     *
     * <p>NOTE: On API levels below 26, the concept of vertical accuracy does not exist. In order to
     * allow for backwards compatibility and testing however, this method will attempt to set a
     * float extra with the key {@link #EXTRA_VERTICAL_ACCURACY} to include vertical accuracy. Be
     * aware that this will overwrite any prior extra value under the same key.
     *
     * @see Location#setVerticalAccuracyMeters(float)
     */
    public static void setVerticalAccuracyMeters(@NonNull Location location,
            float verticalAccuracyM) {
        if (VERSION.SDK_INT >= 26) {
            Api26Impl.setVerticalAccuracyMeters(location, verticalAccuracyM);
        } else {
            getOrCreateExtras(location).putFloat(EXTRA_VERTICAL_ACCURACY, verticalAccuracyM);
        }
    }

    /**
     * Removes the vertical accuracy from the location.
     */
    public static void removeVerticalAccuracy(@NonNull Location location) {
        if (VERSION.SDK_INT >= 33) {
            Api33Impl.removeVerticalAccuracy(location);
        } else if (VERSION.SDK_INT >= 29) {
            Api29Impl.removeVerticalAccuracy(location);
        } else if (VERSION.SDK_INT >= 28) {
            Api28Impl.removeVerticalAccuracy(location);
        } else if (VERSION.SDK_INT >= 26) {
            Api26Impl.removeVerticalAccuracy(location);
        } else {
            removeExtra(location, EXTRA_VERTICAL_ACCURACY);
        }
    }

    /**
     * Returns true if this location has a speed accuracy.
     *
     * @see Location#hasSpeedAccuracy()
     */
    public static boolean hasSpeedAccuracy(@NonNull Location location) {
        if (VERSION.SDK_INT >= 26) {
            return Api26Impl.hasSpeedAccuracy(location);
        } else {
            return containsExtra(location, EXTRA_SPEED_ACCURACY);
        }
    }

    /**
     * Get the estimated speed accuracy of this location in meters per second.
     *
     * <p>NOTE: On API levels below 26, the concept of speed accuracy does not exist. In order to
     * allow for backwards compatibility and testing however, this method will attempt to read a
     * float extra with the key {@link #EXTRA_SPEED_ACCURACY} and return the result.
     *
     * @see Location#getSpeedAccuracyMetersPerSecond()
     */
    public static float getSpeedAccuracyMetersPerSecond(@NonNull Location location) {
        if (VERSION.SDK_INT >= 26) {
            return Api26Impl.getSpeedAccuracyMetersPerSecond(location);
        } else {
            Bundle extras = location.getExtras();
            if (extras == null) {
                return 0.0f;
            }

            return extras.getFloat(EXTRA_SPEED_ACCURACY, 0.0f);
        }
    }

    /**
     * Set the estimated speed accuracy of this location in meters per second.
     *
     * <p>NOTE: On API levels below 26, the concept of speed accuracy does not exist. In order to
     * allow for backwards compatibility and testing however, this method will attempt to set a
     * float extra with the key {@link #EXTRA_SPEED_ACCURACY} to include speed accuracy. Be
     * aware that this will overwrite any prior extra value under the same key.
     *
     * @see Location#setSpeedAccuracyMetersPerSecond(float)
     */
    public static void setSpeedAccuracyMetersPerSecond(@NonNull Location location,
            float speedAccuracyMps) {
        if (VERSION.SDK_INT >= 26) {
            Api26Impl.setSpeedAccuracyMetersPerSecond(location, speedAccuracyMps);
        } else {
            getOrCreateExtras(location).putFloat(EXTRA_SPEED_ACCURACY, speedAccuracyMps);
        }
    }

    /**
     * Removes the speed accuracy from the location.
     */
    public static void removeSpeedAccuracy(@NonNull Location location) {
        if (VERSION.SDK_INT >= 33) {
            Api33Impl.removeSpeedAccuracy(location);
        }  else if (VERSION.SDK_INT >= 29) {
            Api29Impl.removeSpeedAccuracy(location);
        }  else if (VERSION.SDK_INT >= 28) {
            Api28Impl.removeSpeedAccuracy(location);
        } else if (VERSION.SDK_INT >= 26) {
            Api26Impl.removeSpeedAccuracy(location);
        } else {
            removeExtra(location, EXTRA_SPEED_ACCURACY);
        }
    }

    /**
     * Returns true if this location has a bearing accuracy.
     *
     * @see Location#hasBearingAccuracy()
     */
    public static boolean hasBearingAccuracy(@NonNull Location location) {
        if (VERSION.SDK_INT >= 26) {
            return Api26Impl.hasBearingAccuracy(location);
        } else {
            return containsExtra(location, EXTRA_BEARING_ACCURACY);
        }
    }

    /**
     * Get the estimated bearing accuracy of this location in degrees.
     *
     * <p>NOTE: On API levels below 26, the concept of bearing accuracy does not exist. In order to
     * allow for backwards compatibility and testing however, this method will attempt to read a
     * float extra with the key {@link #EXTRA_BEARING_ACCURACY} and return the result.
     *
     * @see Location#getBearingAccuracyDegrees()
     */
    public static float getBearingAccuracyDegrees(@NonNull Location location) {
        if (VERSION.SDK_INT >= 26) {
            return Api26Impl.getBearingAccuracyDegrees(location);
        } else {
            Bundle extras = location.getExtras();
            if (extras == null) {
                return 0.0f;
            }

            return extras.getFloat(EXTRA_BEARING_ACCURACY, 0.0f);
        }
    }

    /**
     * Set the estimated bearing accuracy of this location in degrees.
     *
     * <p>NOTE: On API levels below 26, the concept of bearing accuracy does not exist. In order to
     * allow for backwards compatibility and testing however, this method will attempt to set a
     * float extra with the key {@link #EXTRA_BEARING_ACCURACY} to include bearing accuracy. Be
     * aware that this will overwrite any prior extra value under the same key.
     *
     * @see Location#setBearingAccuracyDegrees(float)
     */
    public static void setBearingAccuracyDegrees(@NonNull Location location,
            float bearingAccuracyD) {
        if (VERSION.SDK_INT >= 26) {
            Api26Impl.setBearingAccuracyDegrees(location, bearingAccuracyD);
        } else {
            getOrCreateExtras(location).putFloat(EXTRA_BEARING_ACCURACY, bearingAccuracyD);
        }
    }

    /**
     * Removes the bearing accuracy from the location.
     */
    public static void removeBearingAccuracy(@NonNull Location location) {
        if (VERSION.SDK_INT >= 33) {
            Api33Impl.removeBearingAccuracy(location);
        } else if (VERSION.SDK_INT >= 29) {
            Api29Impl.removeBearingAccuracy(location);
        } else if (VERSION.SDK_INT >= 28) {
            Api28Impl.removeBearingAccuracy(location);
        } else if (VERSION.SDK_INT >= 26) {
            Api26Impl.removeBearingAccuracy(location);
        } else {
            removeExtra(location, EXTRA_BEARING_ACCURACY);
        }
    }

    /**
     * Returns the Mean Sea Level altitude of the location in meters.
     *
     * <p>This is only valid if {@link #hasMslAltitude(Location)} is true.
     *
     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
     * order to allow for backwards compatibility and testing however, this method will attempt
     * to read a double extra with the key {@link #EXTRA_MSL_ALTITUDE} and return the result.
     *
     * @see Location#getMslAltitudeMeters()
     */
    public static double getMslAltitudeMeters(@NonNull Location location) {
        if (VERSION.SDK_INT >= 34) {
            return Api34Impl.getMslAltitudeMeters(location);
        }
        return getOrCreateExtras(location).getDouble(EXTRA_MSL_ALTITUDE);
    }

    /**
     * Sets the Mean Sea Level altitude of the location in meters.
     *
     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
     * order to allow for backwards compatibility and testing however, this method will attempt
     * to set a double extra with the key {@link #EXTRA_MSL_ALTITUDE} to include Mean Sea Level
     * altitude. Be aware that this will overwrite any prior extra value under the same key.
     *
     * @see Location#setMslAltitudeMeters(double)
     */
    public static void setMslAltitudeMeters(@NonNull Location location,
            double mslAltitudeMeters) {
        if (VERSION.SDK_INT >= 34) {
            Api34Impl.setMslAltitudeMeters(location, mslAltitudeMeters);
        } else {
            getOrCreateExtras(location).putDouble(EXTRA_MSL_ALTITUDE, mslAltitudeMeters);
        }
    }

    /**
     * Returns true if the location has a Mean Sea Level altitude, false otherwise.
     *
     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
     * order to allow for backwards compatibility and testing however, this method will return
     * true if an extra value is with the key {@link #EXTRA_MSL_ALTITUDE}.
     *
     * @see Location#hasMslAltitude()
     */
    public static boolean hasMslAltitude(@NonNull Location location) {
        if (VERSION.SDK_INT >= 34) {
            return Api34Impl.hasMslAltitude(location);
        }
        return containsExtra(location, EXTRA_MSL_ALTITUDE);
    }

    /**
     * Removes the Mean Sea Level altitude from the location.
     *
     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
     * order to allow for backwards compatibility and testing however, this method will attempt
     * to remove any extra value with the key {@link #EXTRA_MSL_ALTITUDE}.
     *
     * @see Location#removeMslAltitude()
     */
    public static void removeMslAltitude(@NonNull Location location) {
        if (VERSION.SDK_INT >= 34) {
            Api34Impl.removeMslAltitude(location);
        } else {
            removeExtra(location, EXTRA_MSL_ALTITUDE);
        }
    }

    /**
     * Returns the estimated Mean Sea Level altitude accuracy in meters of the location at the 68th
     * percentile confidence level. This means that there is 68% chance that the true Mean Sea Level
     * altitude of the location falls within {@link #getMslAltitudeMeters(Location)} +/- this
     * uncertainty.
     *
     * <p>This is only valid if {@link #hasMslAltitudeAccuracy(Location)} is true.
     *
     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
     * exist. In order to allow for backwards compatibility and testing however, this method will
     * attempt to read a float extra with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY} and return
     * the result.
     *
     * @see Location#getMslAltitudeAccuracyMeters()
     */
    public static @FloatRange(from = 0.0) float getMslAltitudeAccuracyMeters(
            @NonNull Location location) {
        if (VERSION.SDK_INT >= 34) {
            return Api34Impl.getMslAltitudeAccuracyMeters(location);
        }
        return getOrCreateExtras(location).getFloat(EXTRA_MSL_ALTITUDE_ACCURACY);
    }

    /**
     * Sets the Mean Sea Level altitude accuracy of the location in meters.
     *
     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
     * exist. In order to allow for backwards compatibility and testing however, this method will
     * attempt to set a float extra with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY} to include
     * Mean Sea Level altitude accuracy. Be aware that this will overwrite any prior extra value
     * under the same key.
     *
     * @see Location#setMslAltitudeAccuracyMeters(float)
     */
    public static void setMslAltitudeAccuracyMeters(@NonNull Location location,
            @FloatRange(from = 0.0) float mslAltitudeAccuracyMeters) {
        if (VERSION.SDK_INT >= 34) {
            Api34Impl.setMslAltitudeAccuracyMeters(location, mslAltitudeAccuracyMeters);
        } else {
            getOrCreateExtras(location).putFloat(EXTRA_MSL_ALTITUDE_ACCURACY,
                    mslAltitudeAccuracyMeters);
        }
    }

    /**
     * Returns true if the location has a Mean Sea Level altitude accuracy, false otherwise.
     *
     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
     * exist. In order to allow for backwards compatibility and testing however, this method will
     * return true if an extra value is with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY}.
     *
     * @see Location#hasMslAltitudeAccuracy()
     */
    public static boolean hasMslAltitudeAccuracy(@NonNull Location location) {
        if (VERSION.SDK_INT >= 34) {
            return Api34Impl.hasMslAltitudeAccuracy(location);
        }
        return containsExtra(location, EXTRA_MSL_ALTITUDE_ACCURACY);
    }

    /**
     * Removes the Mean Sea Level altitude accuracy from the location.
     *
     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
     * exist. In order to allow for backwards compatibility and testing however, this method will
     * attempt to remove any extra value with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY}.
     *
     * @see Location#removeMslAltitudeAccuracy()
     */
    public static void removeMslAltitudeAccuracy(@NonNull Location location) {
        if (VERSION.SDK_INT >= 34) {
            Api34Impl.removeMslAltitudeAccuracy(location);
        } else {
            removeExtra(location, EXTRA_MSL_ALTITUDE_ACCURACY);
        }
    }

    /**
     * Returns true if this location is marked as a mock location. If this location comes from the
     * Android framework, this indicates that the location was provided by a test location provider,
     * and thus may not be related to the actual location of the device.
     *
     * <p>NOTE: On API levels below 18, the concept of a mock location does not exist. In order to
     * allow for backwards compatibility and testing however, this method will attempt to read a
     * boolean extra with the key {@link #EXTRA_IS_MOCK} and use the result to determine whether
     * this should be considered a mock location.
     *
     * @see android.location.LocationManager#addTestProvider
     */
    @SuppressWarnings("deprecation")
    public static boolean isMock(@NonNull Location location) {
        if (VERSION.SDK_INT >= 31) {
            return Api31Impl.isMock(location);
        } else {
            return location.isFromMockProvider();
        }
    }

    /**
     * Sets whether this location is marked as a mock location.
     *
     * <p>NOTE: On API levels below 18, the concept of a mock location does not exist. In order to
     * allow for backwards compatibility and testing however, this method will attempt to set a
     * boolean extra with the key {@link #EXTRA_IS_MOCK} to mark the location as mock. Be aware that
     * this will overwrite any prior extra value under the same key.
     */
    @SuppressLint("BanUncheckedReflection")
    public static void setMock(@NonNull Location location, boolean mock) {
        try {
                getSetIsFromMockProviderMethod().invoke(location, mock);
            } catch (NoSuchMethodException e) {
                Error error = new NoSuchMethodError();
                error.initCause(e);
                throw error;
            } catch (IllegalAccessException e) {
                Error error = new IllegalAccessError();
                error.initCause(e);
                throw error;
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
    }

    @RequiresApi(34)
    private static class Api34Impl {

        private Api34Impl() {
        }

        static double getMslAltitudeMeters(Location location) {
            return location.getMslAltitudeMeters();
        }

        static void setMslAltitudeMeters(Location location, double mslAltitudeMeters) {
            location.setMslAltitudeMeters(mslAltitudeMeters);
        }

        static boolean hasMslAltitude(Location location) {
            return location.hasMslAltitude();
        }

        static void removeMslAltitude(Location location) {
            location.removeMslAltitude();
        }

        static float getMslAltitudeAccuracyMeters(Location location) {
            return location.getMslAltitudeAccuracyMeters();
        }

        static void setMslAltitudeAccuracyMeters(Location location,
                float mslAltitudeAccuracyMeters) {
            location.setMslAltitudeAccuracyMeters(mslAltitudeAccuracyMeters);
        }

        static boolean hasMslAltitudeAccuracy(Location location) {
            return location.hasMslAltitudeAccuracy();
        }

        static void removeMslAltitudeAccuracy(Location location) {
            location.removeMslAltitudeAccuracy();
        }
    }

    @RequiresApi(33)
    private static class Api33Impl {

        private Api33Impl() {}

        static void removeVerticalAccuracy(Location location) {
            location.removeVerticalAccuracy();
        }

        static void removeSpeedAccuracy(Location location) {
            location.removeSpeedAccuracy();
        }

        static void removeBearingAccuracy(Location location) {
            location.removeBearingAccuracy();
        }
    }

    @RequiresApi(29)
    private static class Api29Impl {

        private Api29Impl() {}

        static void removeVerticalAccuracy(Location location) {
            if (!location.hasVerticalAccuracy()) {
                return;
            }

            // reflection of non-SDK APIs doesn't work on P+, fallback to resetting the location
            double elapsedRealtimeUncertaintyNs = location.getElapsedRealtimeUncertaintyNanos();
            Api28Impl.removeVerticalAccuracy(location);
            location.setElapsedRealtimeUncertaintyNanos(elapsedRealtimeUncertaintyNs);
        }

        static void removeSpeedAccuracy(Location location) {
            if (!location.hasSpeedAccuracy()) {
                return;
            }

            // reflection of non-SDK APIs doesn't work on P+, fallback to resetting the location
            double elapsedRealtimeUncertaintyNs = location.getElapsedRealtimeUncertaintyNanos();
            Api28Impl.removeSpeedAccuracy(location);
            location.setElapsedRealtimeUncertaintyNanos(elapsedRealtimeUncertaintyNs);
        }

        static void removeBearingAccuracy(Location location) {
            if (!location.hasBearingAccuracy()) {
                return;
            }

            // reflection of non-SDK APIs doesn't work on P+, fallback to resetting the location
            double elapsedRealtimeUncertaintyNs = location.getElapsedRealtimeUncertaintyNanos();
            Api28Impl.removeBearingAccuracy(location);
            location.setElapsedRealtimeUncertaintyNanos(elapsedRealtimeUncertaintyNs);
        }
    }

    @RequiresApi(28)
    private static class Api28Impl {

        private Api28Impl() {}

        static void removeVerticalAccuracy(Location location) {
            if (!location.hasVerticalAccuracy()) {
                return;
            }

            // reflection of non-SDK APIs doesn't work on P+, fallback to resetting the location
            // (which unfortunately means a new Bundle will be created)

            String provider = location.getProvider();
            long time = location.getTime();
            long elapsedRealtimeNs = location.getElapsedRealtimeNanos();
            double latitude = location.getLatitude();
            double longitude = location.getLongitude();
            boolean hasAltitude = location.hasAltitude();
            double altitude = location.getAltitude();
            boolean hasSpeed = location.hasSpeed();
            float speed = location.getSpeed();
            boolean hasBearing = location.hasBearing();
            float bearing = location.getBearing();
            boolean hasAccuracy = location.hasAccuracy();
            float accuracy = location.getAccuracy();
            boolean hasSpeedAccuracy = location.hasSpeedAccuracy();
            float speedAccuracy = location.getSpeedAccuracyMetersPerSecond();
            boolean hasBearingAccuracy = location.hasBearingAccuracy();
            float bearingAccuracy = location.getBearingAccuracyDegrees();
            Bundle extras = location.getExtras();

            location.reset();
            location.setProvider(provider);
            location.setTime(time);
            location.setElapsedRealtimeNanos(elapsedRealtimeNs);
            location.setLatitude(latitude);
            location.setLongitude(longitude);
            if (hasAltitude) {
                location.setAltitude(altitude);
            }
            if (hasSpeed) {
                location.setSpeed(speed);
            }
            if (hasBearing) {
                location.setBearing(bearing);
            }
            if (hasAccuracy) {
                location.setAccuracy(accuracy);
            }
            if (hasSpeedAccuracy) {
                location.setSpeedAccuracyMetersPerSecond(speedAccuracy);
            }
            if (hasBearingAccuracy) {
                location.setBearingAccuracyDegrees(bearingAccuracy);
            }
            if (extras != null) {
                location.setExtras(extras);
            }
        }

        static void removeSpeedAccuracy(Location location) {
            if (!location.hasSpeedAccuracy()) {
                return;
            }

            // reflection of non-SDK APIs doesn't work on P+, fallback to resetting the location
            // (which unfortunately means a new Bundle will be created)

            String provider = location.getProvider();
            long time = location.getTime();
            long elapsedRealtimeNs = location.getElapsedRealtimeNanos();
            double latitude = location.getLatitude();
            double longitude = location.getLongitude();
            boolean hasAltitude = location.hasAltitude();
            double altitude = location.getAltitude();
            boolean hasSpeed = location.hasSpeed();
            float speed = location.getSpeed();
            boolean hasBearing = location.hasBearing();
            float bearing = location.getBearing();
            boolean hasAccuracy = location.hasAccuracy();
            float accuracy = location.getAccuracy();
            boolean hasVerticalAccuracy = location.hasVerticalAccuracy();
            float verticalAccuracy = location.getVerticalAccuracyMeters();
            boolean hasBearingAccuracy = location.hasBearingAccuracy();
            float bearingAccuracy = location.getBearingAccuracyDegrees();
            Bundle extras = location.getExtras();

            location.reset();
            location.setProvider(provider);
            location.setTime(time);
            location.setElapsedRealtimeNanos(elapsedRealtimeNs);
            location.setLatitude(latitude);
            location.setLongitude(longitude);
            if (hasAltitude) {
                location.setAltitude(altitude);
            }
            if (hasSpeed) {
                location.setSpeed(speed);
            }
            if (hasBearing) {
                location.setBearing(bearing);
            }
            if (hasAccuracy) {
                location.setAccuracy(accuracy);
            }
            if (hasVerticalAccuracy) {
                location.setVerticalAccuracyMeters(verticalAccuracy);
            }
            if (hasBearingAccuracy) {
                location.setBearingAccuracyDegrees(bearingAccuracy);
            }
            if (extras != null) {
                location.setExtras(extras);
            }
        }

        static void removeBearingAccuracy(Location location) {
            if (!location.hasBearingAccuracy()) {
                return;
            }

            // reflection of non-SDK APIs doesn't work on P+, fallback to resetting the location
            // (which unfortunately means a new Bundle will be created)

            String provider = location.getProvider();
            long time = location.getTime();
            long elapsedRealtimeNs = location.getElapsedRealtimeNanos();
            double latitude = location.getLatitude();
            double longitude = location.getLongitude();
            boolean hasAltitude = location.hasAltitude();
            double altitude = location.getAltitude();
            boolean hasSpeed = location.hasSpeed();
            float speed = location.getSpeed();
            boolean hasBearing = location.hasBearing();
            float bearing = location.getBearing();
            boolean hasAccuracy = location.hasAccuracy();
            float accuracy = location.getAccuracy();
            boolean hasVerticalAccuracy = location.hasVerticalAccuracy();
            float verticalAccuracy = location.getVerticalAccuracyMeters();
            boolean hasSpeedAccuracy = location.hasSpeedAccuracy();
            float speedAccuracy = location.getSpeedAccuracyMetersPerSecond();
            Bundle extras = location.getExtras();

            location.reset();
            location.setProvider(provider);
            location.setTime(time);
            location.setElapsedRealtimeNanos(elapsedRealtimeNs);
            location.setLatitude(latitude);
            location.setLongitude(longitude);
            if (hasAltitude) {
                location.setAltitude(altitude);
            }
            if (hasSpeed) {
                location.setSpeed(speed);
            }
            if (hasBearing) {
                location.setBearing(bearing);
            }
            if (hasAccuracy) {
                location.setAccuracy(accuracy);
            }
            if (hasVerticalAccuracy) {
                location.setVerticalAccuracyMeters(verticalAccuracy);
            }
            if (hasSpeedAccuracy) {
                location.setBearingAccuracyDegrees(speedAccuracy);
            }
            if (extras != null) {
                location.setExtras(extras);
            }
        }
    }

    @RequiresApi(26)
    private static class Api26Impl {

        private Api26Impl() {
        }

        static boolean hasVerticalAccuracy(Location location) {
            return location.hasVerticalAccuracy();
        }

        static float getVerticalAccuracyMeters(Location location) {
            return location.getVerticalAccuracyMeters();
        }

        static void setVerticalAccuracyMeters(Location location, float verticalAccuracyM) {
            location.setVerticalAccuracyMeters(verticalAccuracyM);
        }

        static void removeVerticalAccuracy(Location location) {
            try {
                byte fieldsMask = getFieldsMaskField().getByte(location);
                fieldsMask = (byte) (fieldsMask & ~getHasVerticalAccuracyMask());
                getFieldsMaskField().setByte(location, fieldsMask);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                Error error = new IllegalAccessError();
                error.initCause(e);
                throw error;
            }
        }

        static boolean hasSpeedAccuracy(Location location) {
            return location.hasSpeedAccuracy();
        }

        static float getSpeedAccuracyMetersPerSecond(Location location) {
            return location.getSpeedAccuracyMetersPerSecond();
        }

        static void setSpeedAccuracyMetersPerSecond(Location location, float speedAccuracyMps) {
            location.setSpeedAccuracyMetersPerSecond(speedAccuracyMps);
        }

        static void removeSpeedAccuracy(Location location) {
            try {
                byte fieldsMask = getFieldsMaskField().getByte(location);
                fieldsMask = (byte) (fieldsMask & ~getHasSpeedAccuracyMask());
                getFieldsMaskField().setByte(location, fieldsMask);
            } catch (NoSuchFieldException e) {
                Error error = new NoSuchFieldError();
                error.initCause(e);
                throw error;
            } catch (IllegalAccessException e) {
                Error error = new IllegalAccessError();
                error.initCause(e);
                throw error;
            }
        }

        static boolean hasBearingAccuracy(Location location) {
            return location.hasBearingAccuracy();
        }

        static float getBearingAccuracyDegrees(Location location) {
            return location.getBearingAccuracyDegrees();
        }

        static void setBearingAccuracyDegrees(Location location, float bearingAccuracyD) {
            location.setBearingAccuracyDegrees(bearingAccuracyD);
        }

        static void removeBearingAccuracy(Location location) {
            try {
                byte fieldsMask = getFieldsMaskField().getByte(location);
                fieldsMask = (byte) (fieldsMask & ~getHasBearingAccuracyMask());
                getFieldsMaskField().setByte(location, fieldsMask);
            } catch (NoSuchFieldException e) {
                Error error = new NoSuchFieldError();
                error.initCause(e);
                throw error;
            } catch (IllegalAccessException e) {
                Error error = new IllegalAccessError();
                error.initCause(e);
                throw error;
            }
        }
    }

    private static Method getSetIsFromMockProviderMethod() throws NoSuchMethodException {
        if (sSetIsFromMockProviderMethod == null) {
            sSetIsFromMockProviderMethod = Location.class.getDeclaredMethod("setIsFromMockProvider",
                    boolean.class);
            sSetIsFromMockProviderMethod.setAccessible(true);
        }

        return sSetIsFromMockProviderMethod;
    }

    @SuppressLint("BlockedPrivateApi")
    static Field getFieldsMaskField() throws NoSuchFieldException {
        if (sFieldsMaskField == null) {
            sFieldsMaskField = Location.class.getDeclaredField("mFieldsMask");
            sFieldsMaskField.setAccessible(true);
        }

        return sFieldsMaskField;
    }

    @SuppressLint("SoonBlockedPrivateApi")
    static int getHasSpeedAccuracyMask() throws NoSuchFieldException,
            IllegalAccessException {
        if (sHasSpeedAccuracyMask == null) {
            Field hasSpeedAccuracyMaskField = Location.class.getDeclaredField(
                    "HAS_SPEED_ACCURACY_MASK");
            hasSpeedAccuracyMaskField.setAccessible(true);
            sHasSpeedAccuracyMask = hasSpeedAccuracyMaskField.getInt(null);
        }

        return sHasSpeedAccuracyMask;
    }

    @SuppressLint("SoonBlockedPrivateApi")
    static int getHasBearingAccuracyMask()
            throws NoSuchFieldException, IllegalAccessException {
        if (sHasBearingAccuracyMask == null) {
            Field hasBearingAccuracyMaskField = Location.class.getDeclaredField(
                    "HAS_BEARING_ACCURACY_MASK");
            hasBearingAccuracyMaskField.setAccessible(true);
            sHasBearingAccuracyMask = hasBearingAccuracyMaskField.getInt(null);
        }

        return sHasBearingAccuracyMask;
    }

    @SuppressLint("SoonBlockedPrivateApi")
    static int getHasVerticalAccuracyMask()
            throws NoSuchFieldException, IllegalAccessException {
        if (sHasVerticalAccuracyMask == null) {
            Field hasVerticalAccuracyMaskField = Location.class.getDeclaredField(
                    "HAS_VERTICAL_ACCURACY_MASK");
            hasVerticalAccuracyMaskField.setAccessible(true);
            sHasVerticalAccuracyMask = hasVerticalAccuracyMaskField.getInt(null);
        }

        return sHasVerticalAccuracyMask;
    }

    private static Bundle getOrCreateExtras(@NonNull Location location) {
        Bundle extras = location.getExtras();
        if (extras == null) {
            location.setExtras(new Bundle());
            extras = location.getExtras();
        }

        return extras;
    }

    private static boolean containsExtra(@NonNull Location location, String key) {
        Bundle extras = location.getExtras();
        return extras != null && extras.containsKey(key);
    }

    private static void removeExtra(@NonNull Location location, String key) {
        Bundle extras = location.getExtras();
        if (extras != null) {
            extras.remove(key);
            if (extras.isEmpty()) {
                location.setExtras(null);
            }
        }
    }

    @RequiresApi(31)
    static class Api31Impl {
        private Api31Impl() {
            // This class is not instantiable.
        }

        static boolean isMock(Location location) {
            return location.isMock();
        }
    }
}
